Eventloop와 비동기
자바스크립트 엔진은 단 하나의 Call Stack(호출 스택) 을 가진다. 그래서 싱글 스레드(singe-thread) 언어라고 부르며, 한 순간에는 오직 하나의 작업만 실행할 수 있다.
즉, 함수가 실행되는 동안 다른 함수가 동시에 실행될 수 없으며 자바스크립트의 기본 실행 모델은 “동기(synchronous)”다.
그렇다면 비동기 작업은 어떻게 처리될까?
비동기와 이벤트루프
비동기 작업은 완료된 후 바로 실행되지 않는다. 브라우저(또는 Node.js)가 작업을 처리한 뒤 Task Queue에 콜백을 넣고, 콜 스택(Call Stack)이 완전히 비워질 때까지 기다린다.
Task Queue는 두 종류로 나뉜다.
Micro Task Queue: Promise.then, MutationObserver 등Macro Task Queue: setTimeout, setInterval, I/O 등
이벤트 루프(Event Loop)는 다음 규칙으로 동작한다.
Call Stack이 비면,
Micro Task Queue의 모든 작업을 콜스택에 넣고,
그 다음에야 Macro Task Queue에서 작업 하나를 콜스택에 넣는다.
그렇기 때문에 setTimeOut은 정확하지 않은 것이다.
setTimeout(() => console.log('done'), 10);
이 코드는 10ms 뒤에 실행되는 것이 아니라, 10ms 뒤에 Macro Task Queue에 들어갈 뿐이다. 실제로 실행되는 시점은 콜 스택이 비워지고, Micro Task가 모두 처리된 후, 이벤트 루프가 해당 작업을 스택에 넣어주는 순간이다.
따라서 최소 지연 시간(minimum delay)은 보장하지만 정확한 실행 시점을 보장하지는 않는다.
Main Thread를 점유하지 않기
큰 작업이 있게 되면 CallStack이 비워지지 않는다.
계속 Main Thread를 점유하고 있기 때문에, Single Thread JS에서는 다른 작업을 할 수도 없다.
또, 브라우저도 렌더링을 위해 Main Thread를 쓰기 때문에 렌더링에도 문제가 생긴다.
그러면 큰 작업이 예상될 때는 어떻게 해야할까?
1. setTimeout(0)
setTimeout(() => {
// 오래 걸리는 작업
}, 0);
setTimeout(0) 처럼 TaskQueue로 작업을 빼는 방법이 있다.
말 그대로 큰 작업은 뒤로 빼두고 다른 작업 먼저 실행하라는 의미다.
이 작업은 다른 콜스택이 비워줘야면 TaskQueue에서 올라와서 실행된다.
그래서 병목을 막을수 있다.
2. Web Worker
다른 방법은 Worker를 사용하는 것이다. Worker는 브라우저의 Renderer Process에서 동작하지만, Main Thread가 아니라 Work Thread에서 독립적으로 실행된다.
대표적으로 영상 처리, 이미지 변환, 대용량 연산 등에 웹 워커를 고민해보자
JS로 애니메이션을 조작할때, requestAnimationFrame
지속되어야하는 애니메이션인 경우 setInterval, setTimeout을 쓰면 프레임 타이밍과 맞지 않아 애니메이션이 끊어지는 현상이 발생한다.
requestAnimationFrame은 다른 TaskQueue를 갖는다.
rAF Queue는 주사율 타이밍, 즉 렌더링 때 실행되는 것을 보장해준다.
브라우저가 렌더링을 진행하기 직전에 호출되어
레이아웃/페인트 타이밍과 동기화된다.
그래서 애니메이션 시각적 품질이 좋아지고, 버벅임(jank)이 줄어든다.